Silent Session Check
The silent session check lets a site on abair.ie find out whether the visitor is already signed
in at auth.abair.ie, and get a session of its own if they are, without navigating anywhere.
The site loads auth.abair.ie/session-check in a hidden iframe, and that page answers over
postMessage. The same check lets a signed-in site notice that the user has signed out somewhere
else, and sign out too.
The ABAIR website uses it on every page that reads the sign-in state through useAuthSession,
which today is the synthesis and speech-to-speech pages.
Sites using it
| Site | Picks up a sign-in | Follows sign-outs | Where |
|---|---|---|---|
abair.ie, www.abair.ie, preview.abair.ie | Yes, once per tab | Yes | abair_website: hooks/useAuthSession.ts |
leighdom.abair.ie | Yes, every 10 minutes at most | Yes | file-tts-tool: components/auth/SilentSignIn.tsx |
mileglornanog.abair.ie | Yes, once per tab, and again on /login | No | mile-glor-na-nog: lib/AuthContext.tsx, app/login/page.tsx |
Míle Glór na nÓg doesn't follow sign-outs because its own sign-in is the
Embedded Flow, which keeps its session apart from the
auth.abair.ie sign-in. Someone who signs in through it is signed out as far as
auth.abair.ie can tell, so following sign-outs would sign them out again at once. Its /login
page runs the check alongside the embedded form, and moves the visitor on if the check signs
them in first.
What the website does
| Visitor on the site | When the site checks | Outcome |
|---|---|---|
| Not signed in | Once per browser tab, on the first page that uses useAuthSession | If they're signed in at auth.abair.ie, the site redeems a one-time token and is signed in, no redirect. Otherwise nothing changes |
| Signed in | On page load, and whenever the tab becomes visible again, at most once a minute | If auth.abair.ie says they've signed out, the site signs out locally. Anything else changes nothing |
- Once per tab. The signed-out check sets
abair.ssoCheckedinsessionStoragebefore it runs, so a visitor who isn't signed in doesn't pay for the iframe on every page. A tab that has already checked won't check again: someone who signs in atauth.abair.iein another tab picks it up in a new tab, or by pressing Sign in, which goes straight through the Redirect Flow without showing the form. - Timeout. The site waits up to 8 seconds for an answer, then gives up and removes the iframe.
A timeout is treated like
unknown: nothing happens.
Sequence
Message contract
All three messages carry a type. Anything else, or anything from an unexpected window or origin,
is ignored.
1. Frame → site: ready. Sent as soon as the page loads, addressed to "*". It carries no data,
so it's safe to send before the frame knows who its parent is.
{ type: "SESSION_CHECK_READY" }
2. Site → frame: request. Addressed to the auth origin only. nonce is a fresh random string;
handoff says whether the site wants a sign-in token (it defaults to true, for sites built
before the field existed). A site that is already signed in sends handoff: false, so no token is
minted for nothing.
{ type: "SESSION_CHECK_REQUEST", nonce: "<random>", handoff: true }
3. Frame → site: response. Addressed to the exact origin that sent the request, never "*".
The page answers once per load, so create a new iframe for every check.
{
type: "SESSION_CHECK_RESPONSE",
nonce: "<the nonce you sent>",
status: "signed_in" | "signed_out" | "unknown",
handoff: { token_hash: "…", type: "magiclink" } | null,
}
Status
status | Meaning | What a site should do |
|---|---|---|
signed_in | There is a session at auth.abair.ie, and the server confirmed it's still live | If handoff is present, redeem it with verifyOtp |
signed_out | There is definitely no session: never signed in, signed out, or revoked by a sign-out elsewhere. A revoked session is also cleared from auth.abair.ie itself | If the site is signed in, sign out locally |
unknown | It couldn't tell: offline, a network error, or Supabase unreachable | Nothing. Never sign anyone out on this |
A response without a status (from an older version of the page) should be treated as unknown.
handoff can be null even when the status is signed_in: either the site sent handoff: false,
or the gateway couldn't issue a token. There is deliberately no fallback to the session tokens
here. A silent sign-in that can't give the site its own session leaves it signed out.
Why a one-time token instead of the session
Copying auth.abair.ie's access_token + refresh_token into another site with setSession
makes both sites share one session and one refresh token. Whichever refreshes first invalidates
the other's copy, and Supabase's refresh-token reuse detection can then end the session everywhere.
Instead, auth.abair.ie trades the user's access token at the gateway for a magic-link
token_hash (Supabase generates it, but no email is sent). The site redeems it with
supabase.auth.verifyOtp({ token_hash, type }) and gets a brand-new session of its own, which
refreshes independently.
The gateway endpoint (POST https://api.abair.ie/v4/session-handoff) only issues a token when:
- the call comes from
https://auth.abair.ie(the gateway'sALLOWED_ORIGINS); - the access token is valid and was issued in the last 120 seconds
(
SESSION_HANDOFF_MAX_TOKEN_AGE_SECONDS).auth.abair.ierefreshes right before calling, so a stolen access token on its own can't be turned into a session.
The token is single-use: once redeemed, it can't be redeemed again.
The same hand-off in the redirect flow
The one-time token also works with the normal Redirect Flow. Add
handoff=token_hash to the sign-in link:
https://auth.abair.ie/?ref=https://abair.ie/auth/callback&app_name=Abair&handoff=token_hash
After sign-in, the user comes back with the token after the #, which browsers never send to
the server, so it stays out of access logs:
https://abair.ie/auth/callback#token_hash=…&type=magiclink
If a token couldn't be issued, sign-in still works: auth.abair.ie falls back to the session
tokens in the query string, as described in the Redirect Flow. The website's /auth/callback
page handles both. The handoff parameter is carried through detours to sign-up or the profile
page.
Sites that can't use the iframe
A site that isn't on the session-check allowlist can still do a silent check with a full-page
round trip, by adding prompt=none to the sign-in link. If the user is signed in, they come
straight back with a session as usual. If not, they come straight back with ?error=login_required
instead of being shown the sign-in form. Both only happen when ref is a trusted ABAIR address;
any other ref gets the normal sign-in page.
Security
- Who can receive an answer is enforced twice. The
/session-checkframe-ancestorsCSP stops any other site from embedding the page at all. The page also checks the origin the browser reports on each request (isSessionCheckOrigin) and addresses its reply to that origin only. - The allowlist is exact origins, not every
*.abair.iesite:https://abair.ie,https://www.abair.ie,https://preview.abair.ie,https://leighdom.abair.ieandhttps://mileglornanog.abair.ie. Any origin on it can get a session for anyone signed in, without the user doing anything, so only add sites that are actively maintained. - The site trusts only its own iframe. It accepts a response only from the iframe it created
(
event.source), from the auth origin, carrying the nonce it sent. - The iframe is sandboxed (
allow-scripts allow-same-origin): it can't navigate the site's page, open popups or submit forms. - The page runs no third-party scripts. Its CSP allows scripts from
auth.abair.ieonly, and connections only to Supabase andapi.abair.ie. Cloudflare injects an inline bot-detection script into everyauth.abair.iepage; on/session-checkthe browser blocks it, so a CSP error for it in the console is expected and harmless. - Only a definite
signed_outsigns a site out, so an outage or a slow network never signs anyone out.
The check only works for sites on abair.ie. Browsers give an iframe of auth.abair.ie the same
storage as auth.abair.ie itself only when the page around it is on the same site (abair.ie).
Embedded in a site on any other domain, the iframe gets separate, empty storage and will always
answer signed_out.
Shared sessions
A site that follows sign-outs must not hold a copy of auth.abair.ie's own session. The
Redirect Flow without handoff=token_hash hands the site
auth.abair.ie's own access_token + refresh_token, so the two share one refresh token.
The check loads auth.abair.ie often, and it refreshes its session when the access token is
due. When the site and auth.abair.ie both refresh the same session, the second one presents a
refresh token that has already been used. Supabase takes that as a stolen token and ends the
session. The check then answers signed_out, and the site signs itself out, typically about an
hour after signing in.
So a site that follows sign-outs should ask for the one-time token on its normal sign-in too
(handoff=token_hash), as the ABAIR website and Léigh Dom do. A site whose sign-in doesn't
sign the visitor in on auth.abair.ie at all, like Míle Glór na nÓg with its embedded form,
should only use the check to pick up a sign-in.
Adding a site
A new site has to be added in three places. If one is missing, the check fails silently: the iframe is blocked or never answers, and the site times out after 8 seconds.
SESSION_CHECK_ORIGINSin the auth repo'ssrc/config/authOrigins.js.- The
/session-checkframe-ancestorsline in the auth repo'spublic/_headers, with the same origins. - The site's own CSP:
frame-src https://auth.abair.ie, plusconnect-srcfor Supabase soverifyOtpcan run. For the ABAIR website this lives in the nginx config ininfrastructure-automation(networking/nginx-config/abair.ieandwww.abair.ie).
Local development
A development build of auth (npm run dev, on http://localhost:5173 by default) also accepts
http://localhost:3000 and http://localhost:3006 as parents. Point the website at it with
NEXT_PUBLIC_ABAIR_AUTH_URL=http://localhost:5173.
Locally, the check can report status but can't hand off a session: the hand-off request
always goes to https://api.abair.ie, which only accepts calls from https://auth.abair.ie. The
production auth.abair.ie doesn't answer localhost either, so test the full sign-in on
preview.abair.ie.
Where the code lives
| Piece | Repo | File |
|---|---|---|
| The session-check page | auth | src/pages/SessionCheckPage.jsx |
| Allowed sites | auth | src/config/authOrigins.js (SESSION_CHECK_ORIGINS), src/utils/redirect.js (isSessionCheckOrigin) |
CSP for /session-check | auth | public/_headers |
| Requesting a token | auth | src/services/sessionHandoff/requestSessionHandoff.js |
handoff=token_hash and prompt=none | auth | src/pages/RedirectAuthPage.jsx |
| Issuing a token | api-gateway-production | routes/sessionHandoff.js |
| Asking the iframe | abair_website | lib/ssoSessionCheck.ts |
| Redeeming a token | abair_website | lib/sessionHandoff.ts |
| When to check | abair_website | hooks/useAuthSession.ts |
| Redirect sign-in return | abair_website | app/auth/callback/page.tsx |